﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Gargoyle.Roton
{
    public delegate void BufferFullHandler(object sender, BufferFullArgs e);
    
    /// <summary>
    /// The base audio interface for Roton.
    /// </summary>
    public class Audio
    {
        protected const Int16 Amplitude = (Int16)(Int16.MaxValue * 0.5);
        protected const float TickLength = 56.25f;
        protected const float SilenceLength = TickLength / 4;
        protected double AudioVolume = Convert.ToDouble(ConfigManager.GetValue("Audio", "Volume", "0.8")) / 2;  // square waves are unbearably loud
        private bool EnableAudio = true;
        protected AudioWaveGen.WaveTypes waveType;

        /// <summary>
        /// This event fires when the active audio buffer is full.
        /// </summary>
        public event BufferFullHandler BufferFull;

        /// <summary>
        /// Samples per second.
        /// </summary>
        protected int SampleRate { get; private set; }

        /// <summary>
        /// The size of the audio buffer in samples.
        /// </summary>
        protected int BufferSize { get; private set; }

        /// <summary>
        /// The number of bits per sample in the audio stream.
        /// </summary>
        protected BitDepth BitsPerSample { get; private set; }

        /// <summary>
        /// The number of bytes used per sample.
        /// </summary>
        protected int BytesPerSample {
            get
            {
                switch(BitsPerSample)
                {
                    case BitDepth.Signed16Bit:  return 2;
                    default:                    return -1;
                }
            }
        }

        /// <summary>
        /// The number of samples per audio tick.
        /// </summary>
        protected int SamplesPerTick
        {
            get
            {
                return (int)(SampleRate * (TickLength / 1000));
            }
        }

        /// <summary>
        /// A list containing data for all of the percussion sounds.
        /// </summary>
        protected List<byte[]> Samples = new List<byte[]>();

        private byte[] bufferData;
        private MemoryStream Buffer;

        /// <summary>
        /// The bit depths supported by the audio interface.
        /// </summary>
        public enum BitDepth {
            Unknown, Signed16Bit
        }

        /// <summary>
        /// Creates a blank audio interface with no audio backends.
        /// </summary>
        public Audio()
        {
            EnableAudio = false;
        }

        /// <summary>
        /// Create a new Audio interface.
        /// </summary>
        /// <param name="newSampleRate">Samples per second.</param>
        /// <param name="newBitsPerSample">Number of bits per sample.</param>
        /// <param name="newBufferSize">Size of the buffer in samples.</param>
        public Audio(int newSampleRate, BitDepth newBitsPerSample, int newBufferSize)
        {
            SampleRate = newSampleRate;
            BufferSize = newBufferSize;
            BitsPerSample = newBitsPerSample;

            // create a backing store so that the buffer never automatically grows
            // (note that the length has to align with the length of one audio tick,
            // otherwise the percussion sounds will not play correctly)
            bufferData = new byte[BufferSize * BytesPerSample];
            Buffer = new MemoryStream(bufferData);

            // Get the type of wave that should be generated.
            string newWaveType = ConfigManager.GetValue("Audio", "WaveType", "PureSquare");
            switch(newWaveType)
            {
                case "PureSawtooth":
                    waveType = AudioWaveGen.WaveTypes.PureSawtooth;
                    break;
                case "PureSine":
                    waveType = AudioWaveGen.WaveTypes.PureSine;
                    break;
                case "PureSquare":
                default:
                    waveType = AudioWaveGen.WaveTypes.PureSquare;
                    break;
                case "PureTriangle":
                    waveType = AudioWaveGen.WaveTypes.PureTriangle;
                    break;
                case "RampedSquare":
                    waveType = AudioWaveGen.WaveTypes.RampedSquare;
                    break;
                case "Squaretooth":
                    waveType = AudioWaveGen.WaveTypes.Squaretooth;
                    break;
            }
        }

        ~Audio()
        {
            if(EnableAudio)
                Buffer.Close();
        }

        /// <summary>
        /// Deallocate all percussion samples.
        /// </summary>
        public virtual void FreeSounds()
        {
            Samples.Clear();
        }

        /// <summary>
        /// Allocate a percussion sample. The length is retrieved from Stream.Length.
        /// </summary>
        /// <param name="source">The stream from which to load the sample.</param>
        /// <param name="musical">Indicates whether the stream will be used in music and should be tick-aligned.</param>
        /// <returns>Returns the index of the loaded sample.</returns>
        public int LoadSound(Stream source, bool musical)
        {
            return LoadSound(source, (int)source.Length, musical);
        }

        /// <summary>
        /// Allocate a percussion sample.
        /// </summary>
        /// <param name="source">The stream from which to load the sample.</param>
        /// <param name="length">The length, in bytes, to read from the stream.</param>
        /// <param name="musical">Indicates whether the stream will be used in music and should be tick-aligned.</param>
        /// <returns>Returns the index of the loaded sample.</returns>
        public virtual int LoadSound(Stream source, int length, bool musical)
        {
            int index = Samples.Count;
            if(!EnableAudio) return index;
            byte[] result;
            
            if(musical)
            {
                int adjustedLength = length;

                adjustedLength = (int)(((length / SamplesPerTick / 2) + 1) * SamplesPerTick) * BytesPerSample;
                result = new byte[adjustedLength];
            }
            else
            {
                result = new byte[length];
            }

            source.Read(result, 0, result.Length);
            Samples.Add(result);

            return index;
        }

        /// <summary>
        /// Play a percussion sample.
        /// </summary>
        /// <param name="index">The index of the sample to play.</param>
        public void PlaySound(int index)
        {
            PlaySound(index, -1);
        }

        /// <summary>
        /// Play a percussion sample for a specified number of ticks..
        /// </summary>
        /// <param name="index">The index of the sample to play.</param>
        /// <param name="ticks">The number of total ticks that the sample should play for.</param>
        public void PlaySound(int index, int ticks)
        {
            if(!EnableAudio) return;

            MemoryStream result = new MemoryStream(Samples[index]);
            int remainingTicks = ticks - ((int)result.Length / BytesPerSample / SamplesPerTick);
            WriteBuffer(ref result);
            result.Dispose();

            // pad the remainder of the play time with silence
            if(remainingTicks > 0) Rest(remainingTicks);
        }
        
        /// <summary>
        /// Generate and play a square wave tone.
        /// </summary>
        /// <param name="freq">The frequency of the tone, in Hz.</param>
        /// <param name="ticks">Indicates how many audio ticks the note should play.</param>
        public void PlayTone(double freq, double ticks)
        {
            if(!EnableAudio) return;

            if(ticks <= 0) return;
            int sampleCount = (int)(ticks * SamplesPerTick);
            int totalBytes = sampleCount * BytesPerSample;
            byte[] soundBytes = new byte[totalBytes];
            short[] soundData = new short[sampleCount];
            
            soundData = AudioWaveGen.GenerateWave(waveType, SampleRate, freq, sampleCount, Amplitude);
            System.Buffer.BlockCopy(soundData, 0, soundBytes, 0, totalBytes);
            MemoryStream soundStream = new MemoryStream(soundBytes);

            WriteBuffer(ref soundStream);
            soundStream.Dispose();
        }

        /// <summary>
        /// Writes a number of audio ticks of silence for #play x.
        /// </summary>
        public void Rest(int ticks)
        {
            if(!EnableAudio) return;

            int sampleCount = (int)(ticks * SamplesPerTick);
            int capacity = sampleCount * BytesPerSample;
            byte[] soundBytes = new byte[capacity];
            MemoryStream result = new MemoryStream(soundBytes);
            BinaryWriter writer = new BinaryWriter(result);
            result.SetLength(sampleCount);

            for(int i = 0; i < sampleCount; i++)
                writer.Write((short)0);

            writer.Flush();
            result.Position = 0;
            WriteBuffer(ref result);
            result.Dispose();
        }
        
        /// <summary>
        /// Writes a single tick of silence to the buffer.
        /// </summary>
        public void Silence()
        {
            if(!EnableAudio) return;

            int sampleCount = (int)(SilenceLength / 1000 * SampleRate);
            int capacity = sampleCount * BytesPerSample;
            byte[] soundBytes = new byte[capacity];
            MemoryStream result = new MemoryStream(soundBytes);
            BinaryWriter writer = new BinaryWriter(result);
            result.SetLength(sampleCount);

            for(int i = 0; i < sampleCount; i++)
                writer.Write((short)0);

            writer.Flush();
            result.Position = 0;
            WriteBuffer(ref result);
            result.Dispose();
        }

        /// <summary>
        /// Stop playing sound.
        /// </summary>
        public void Stop()
        {
            if(!EnableAudio) return;

            StopStream();
        }

        /// <summary>
        /// When overriden in an inherited class, stop the currently playing stream.
        /// </summary>
        protected virtual void StopStream()
        {
        }

        /// <summary>
        /// Writes to the active audio buffers.
        /// </summary>
        /// <param name="soundData">The stream from which to get the data from.</param>
        private void WriteBuffer(ref MemoryStream soundData)
        {
            if(!EnableAudio) return;

            int endByte, remainingBytes, bufferPosition;
            int bufferCapacity = Buffer.Capacity;
            BufferFullArgs e;
                
            while(true)
            {
                remainingBytes = (int)(soundData.Length - soundData.Position);
                bufferPosition = (int)Buffer.Position;

                if(remainingBytes == 0)
                    return;

                endByte = ((remainingBytes + bufferPosition) > (bufferCapacity)) ? bufferCapacity : remainingBytes + bufferPosition;
                for(int i = (int)Buffer.Position; i < endByte; i++)
                    Buffer.WriteByte((byte)soundData.ReadByte());

                if(Buffer.Position >= bufferCapacity)
                {
                    Buffer.Position = 0;
                    e = new BufferFullArgs();
                    e.Buffer = Buffer;
                    BufferFull(this, e);

                    // create a new buffer
                    bufferData = new byte[BufferSize];
                    Buffer = new MemoryStream(bufferData);
                }
            }
        }
    }
}
